Completed
Push — master ( f2a0af...598926 )
by Fike
39s
created

Future.js ➔ ... ➔ this.then   B

Complexity

Conditions 5
Paths 20

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
c 1
b 0
f 0
nc 20
nop 2
dl 0
loc 19
rs 8.8571
1
var Race = require('./Race').Race
2
3
/**
4
 * @callback Future~resolver
5
 *
6
 * @param {Function} resolve
7
 * @param {Function} reject
8
 */
9
10
/**
11
 * @enum
12
 * @readonly
13
 */
14
var Status = {
15
  Pending: 0,
16
  Resolving: 1,
17
  Rejected: 2,
18
  Fulfilled: 3
19
}
20
21
Status.name = function (value) {
22
  return Object.keys(Status).reduce(function (carrier, key) {
23
    return carrier || (Status[key] === value ? key : null)
24
  }, null)
25
}
26
27
/**
28
 * This is a very standard promise interface implementation, but with ability
29
 * to be externally completed or cancelled by `#resolve()` and `#reject()`
30
 * methods.
31
 *
32
 * It is named after Java's CompletableFuture.
33
 *
34
 * **NB:** this implementation doesn't account for cyclic references (which are
35
 * tremendously easy to implement).
36
 *
37
 * @class
38
 * @template T
39
 *
40
 * @param {Future~resolver} [resolver] Standard promise resolver
41
 */
42
function Future (resolver) {
43
  var self = this
44
  var status = Status.Pending
45
  var identity
46
  var propagation = null
47
  var queue = []
48
49
  this.getValue = function () { return identity }
50
51
  this.getStatus = function () { return status }
52
53
  this.hasStatus = function (s) { return status === s }
54
55
  this.isPending = function () { return self.hasStatus(Status.Pending) }
56
  this.isFulfilled = function () { return self.hasStatus(Status.Fulfilled) }
57
  this.isRejected = function () { return self.hasStatus(Status.Rejected) }
58
  this.isResolved = function () {
59
    return status === Status.Fulfilled || status === Status.Rejected
60
  }
61
62
  function schedulePropagation () {
63
    // if already scheduled
64
    if (propagation) { return }
65
    // @see https://promisesaplus.com/#point-34
66
    // setTimeout is used because process.nextTick is not something one mocks
67
    propagation = setTimeout(function () {
68
      propagation = null
69
      propagate()
70
    }, 0)
71
  }
72
73
  function propagate () {
74
    if (!self.isResolved()) { return }
75
    var staged = queue
76
    queue = []
77
    staged.forEach(function (callback) {
78
      callback(status, identity)
79
    })
80
  }
81
82
  function setIdentity (nextStatus, value) {
83
    if (self.isResolved()) { return }
84
    status = nextStatus
85
    identity = value
86
    schedulePropagation()
87
  }
88
89
  var fulfill = setIdentity.bind(self, Status.Fulfilled)
1 ignored issue
show
Unused Code introduced by
The call to bind does not seem necessary since the function setIdentity declared on line 82 does not use this.
Loading history...
90
  var reject = setIdentity.bind(self, Status.Rejected)
1 ignored issue
show
Unused Code introduced by
The call to bind does not seem necessary since the function setIdentity declared on line 82 does not use this.
Loading history...
91
92
  function resolve (nextStatus, value) {
93
    if (!self.isPending()) { return self }
94
    extendedResolutionProcedure(nextStatus, value)
95
    return self
96
  }
97
98
  /**
99
   * Resolves (fulfills) current instance with provided value
100
   *
101
   * @param {T} [value]
102
   * @returns {Future.<T>} Current instance
103
   */
104
  this.fulfill = resolve.bind(this, Status.Fulfilled)
1 ignored issue
show
Unused Code introduced by
The call to bind does not seem necessary since the function resolve declared on line 92 does not use this.
Loading history...
105
106
  this.resolve = this.fulfill
107
108
  /**
109
   * Rejects current instance with provided value
110
   *
111
   * @param {T} [value]
112
   * @returns {Future.<T>} Current instance
113
   */
114
  this.reject = resolve.bind(this, Status.Rejected)
1 ignored issue
show
Unused Code introduced by
The call to bind does not seem necessary since the function resolve declared on line 92 does not use this.
Loading history...
115
116
  if (resolver) { resolver(this.resolve, this.reject) }
117
118
  /**
119
   * @param x
120
   */
121
  function promiseResolutionProcedure (x) {
122
    if (self.isResolved()) { return }
123
    status = Status.Resolving
124
    if (x === self) {
125
      var message = 'Can\'t resolve promise with itself'
126
      return setIdentity(Status.Rejected, new TypeError(message))
127
    }
128
    if (x instanceof Future && x.isResolved()) {
129
      return extendedResolutionProcedure(x.getStatus(), x.getValue())
130
    }
131
    if (!x || (typeof x !== 'function' && typeof x !== 'object')) {
132
      return fulfill(x)
133
    }
134
    var race = new Race(1)
135
    var resolvePromise = race.racer(promiseResolutionProcedure)
136
    var rejectPromise = race.racer(reject)
137
    try {
138
      var then = x.then
139
      if (typeof then !== 'function') { return fulfill(x) }
140
      then.call(x, resolvePromise, rejectPromise)
1 ignored issue
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
141
    } catch (e) {
142
      rejectPromise(e)
1 ignored issue
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
143
    }
144
  }
145
146
  function extendedResolutionProcedure (status, x) {
147
    status === Status.Rejected ? reject(x) : promiseResolutionProcedure(x)
148
    return self
149
  }
150
151
  /**
152
   * Standard then-implementation
153
   *
154
   * @param {Function} [onFulfill]
155
   * @param {Function} [onReject]
156
   * @returns {Future.<T>}
157
   */
158
  this.then = function (onFulfill, onReject) {
159
    if (typeof onFulfill !== 'function') { onFulfill = null }
160
    if (typeof onReject !== 'function') { onReject = null }
161
    if (!onFulfill && !onReject) { return self }
162
    onReject = onReject || function (error) { throw error }
163
    onFulfill = onFulfill || function (value) { return value }
164
    var target = new Future()
165
    var callback = function (status, value) {
166
      var handler = status === Status.Fulfilled ? onFulfill : onReject
167
      try {
168
        target.resolve(handler(value))
169
      } catch (e) {
170
        target.reject(e)
171
      }
172
    }
173
    queue.push(callback)
174
    schedulePropagation()
175
    return target
176
  }
177
178
  this.toString = function () {
179
    var state = Status.name(status)
180
    return 'Future <' + state + (identity ? ':' + identity : '') + '>'
181
  }
182
}
183
184
/**
185
 * Returns promise that awaits all passed promises.
186
 *
187
 * @param {Array.<Promise.<*>|Thenable<*>>} promises
188
 * @returns {Future.<*>}
189
 */
190
Future.all = function (promises) {
191
  return Future.wrap(Promise.all(promises))
192
}
193
194
/**
195
 * Returns result of first resolved promise, be it fulfillment or rejection
196
 *
197
 * @param {Array.<Promise.<*>|Thenable<*>>} promises
198
 * @returns {Future.<*>}
199
 */
200
Future.race = function (promises) {
201
  return Future.wrap(Promise.race(promises))
202
}
203
204
/**
205
 * Returns resolved promise.
206
 *
207
 * @param {*} [value]
208
 * @returns {Future.<*>}
209
 */
210
Future.resolve = function (value) {
211
  return new Future().resolve(value)
212
}
213
214
/**
215
 * Returns rejected promise.
216
 *
217
 * @param {*} [value]
218
 * @returns {Future.<*>}
219
 */
220
Future.reject = function (value) {
221
  return new Future().reject(value)
222
}
223
224
/**
225
 * Wraps given promise in a Future, giving user code an option
226
 * to reject/resolve it.
227
 *
228
 * @param {Promise.<*>|Thenable.<*>} promise
229
 * @returns {Future.<*>}
230
 */
231
Future.wrap = function (promise) {
232
  return new Future(function (resolve, reject) {
233
    // silencing unhandled promise rejection
234
    promise.then(resolve, reject).then(null, function () {})
235
  })
236
}
237
238
Future.Status = Status
239
240
module.exports = {
241
  Future: Future
242
}
243